<?php

/**
 * Body of DirectEdit extension
 *
 * Contains definition of class DirectEdit
 *
 * @author Stephan Gambke
 */
if ( !defined( 'MEDIAWIKI' ) ) {
	die( 'This file is a MediaWiki extension, it is not a valid entry point.' );
}

/**
 * Definition of class DirectEdit
 */
class DirectEdit {

	static function renderDirectEdit ( $parser ) {

		// global $wgParser;

		// set defaults
		$formcontent = '';

		$linkString = null;
		$linkType = 'span';

		$classString = 'directedit-trigger';

		// parse parameters
		$params = func_get_args();
		array_shift( $params ); // don't need the parser

		foreach ( $params as $i => $param ) {

			$elements = explode( '=', $param, 2 );

			$key = trim( $elements[ 0 ] );
			$value = ( count( $elements ) > 1 ) ? trim( $elements[ 1 ] ) : '';

			switch ( $key ) {
				case 'link text':
					$linkString = $parser -> recursiveTagParse( $value );
					break;
				case 'link type':
					$linkType = $parser -> recursiveTagParse( $value );
					break;
				case 'reload':
					$classString .= ' reload';
					break;
				default :
					$formcontent .=
						Xml::tags( 'input', array(
							'type' => 'hidden',
							'name' => $key,
							'value' => urldecode( $value )
							), '' );
			}
		}

		if ( !$linkString ) return null;

		if ( $linkType == 'button' ) {
			$linkElement = Xml::tags( "button", array( 'class' => $classString ), $linkString );
		} elseif ( $linkType == 'link' ) {
			$linkElement = Xml::tags( "a", array( 'class' => $classString, 'href' => "#" ), $linkString );
		} else {
			$linkElement = Xml::tags( "span", array( 'class' => $classString ), $linkString );
		}

		$form = Xml::tags( 'form', array( 'class' => 'directedit-data' ), $formcontent );

		// ensure loading of jQuery and style sheets
		self::loadScriptsForDirectEdit( $parser );

		$output = Xml::tags( "span", array( 'class' => "directedit" ),
				$linkElement .
				Xml::tags( "span", array( 'class' => "directedit-result" ), null ) .
				$form
		);

		// return output HTML
		return $parser -> insertStripItem( $output, $parser -> mStripState );
	}

	static function renderSaveAndContinue ( $parser ) {

		// ensure loading of jQuery and style sheets
		self::loadScriptsForSubmit( $parser );

		$output = Xml::tags( "button", array(
				'class' => "directedit-save_and_continue",
				'disabled' => 'disabled' ),
				wfMsg( 'directedit-saveandcontinue' )
		);


		return $parser -> insertStripItem( $output, $parser -> mStripState );
	}

	static function handleDirectEdit ( $optionsString = null ) {
		$handler = new DirectEditAjaxHandler( $optionsString );
		return $handler -> handleDirectEdit();
	}

	static function handleSubmit ( $optionsString = null ) {
		$handler = new DirectEditAjaxHandler( $optionsString );
		return $handler -> handleSubmit();
	}

	// load scripts and style files for DirectEdit
	private static function loadScriptsForDirectEdit ( &$parser ) {

		global $defScriptPath;

		if ( defined( 'MW_SUPPORTS_RESOURCE_MODULES' ) ) {
			$parser -> getOutput() -> addModules( 'ext.DirectEdit.directedit' );
		} else {

			static $loaded = false;

			// load JavaScript and CSS files only once
			if ( !$loaded ) {

				// load extensions JavaScript
				$parser -> getOutput() -> addHeadItem(
					'<script type="text/javascript" src="' . $defScriptPath
					. '/libs/directedit.js"></script> '
				);

				// load extensions style sheet
				$parser -> getOutput() -> addHeadItem(
					'<link rel="stylesheet" href="' . $defScriptPath
					. '/skins/directedit.css"/> '
				);

				$loaded = true;
			}
		}

		return true;
	}

	// load scripts (mw 1.17+) or add modules (mw 1.16-)
	private static function loadScriptsForSubmit ( &$parser ) {

		global $defScriptPath, $wgOut;

		if ( defined( 'MW_SUPPORTS_RESOURCE_MODULES' ) ) {
			$wgOut -> addModules( 'ext.DirectEdit.submit' );
		} else {

			// load extensions JavaScript
			//$parser -> getOutput() -> addHeadItem(
			$wgOut -> addHeadItem( "directedit-submit-script",
				'<script type="text/javascript" src="' . $defScriptPath
				. '/libs/submit.js"></script> '
			);

			// load extensions style sheet
			$wgOut -> addHeadItem( "directedit-submit-css",
				'<link rel="stylesheet" href="' . $defScriptPath
				. '/skins/submit.css"/> '
			);
		}

		return true;
	}

}

class DirectEditAjaxHandler {

	private $mResult;
	private $mWarnings = array( );  // possibly multiple warnings
	private $mOptions = array( );
	private $mOptionsString = '';
	
	function __construct ( $options ) {

		global $wgParser, $wgUser, $wgVersion;

		$title = new Title();

		if ( version_compare( substr($wgVersion, 0, 4), '1.17', '<' ) ) {
			// TODO: remove work around for bug in MW 1.16

			// get any contents already in putput buffer
			$obcontents = ob_get_contents();

			// perform offensive operation
			eval ('$wgParser -> startExternalParse( & $title, ParserOptions::newFromUser($wgUser), Parser::OT_HTML, true );');

			// clean out the deprecation message (and everything else in the output buffer)
			ob_clean();

			// put former output buffer contents back in
			if ( $obcontents ) echo $obcontents;
			
		} else {
			$wgParser -> startExternalParse( $title, ParserOptions::newFromUser($wgUser), Parser::OT_HTML, true );
		}

		$this -> mOptionsString = $options;
	}

	/**
	 * Handler for the directedit parser function
	 *
	 * @return String
	 */
	function handleDirectEdit () {

		global $wgParser;

		// parse options
		$this -> parseDataFromQueryString( $this -> mOptions, $this -> mOptionsString, true );

		if ( !array_key_exists( 'ok text', $this -> mOptions ) ) {
			$this -> mOptions[ 'ok text' ] = wfMsg( 'directedit-success' );
		}

		if ( !array_key_exists( 'error text', $this -> mOptions ) ) {
			$this -> mOptions[ 'error text' ] = wfMsg( 'directedit-error' );
		}

		if ( !array_key_exists( 'warning text', $this -> mOptions ) ) {
			$this -> mOptions[ 'warning text' ] = wfMsg( 'directedit-warning' );
		}

		if ( $this -> storeSemanticData() ) {
			$this -> mResult = wfMsgReplaceArgs(
					$this -> mOptions[ 'ok text' ],
					array( $this -> mOptions[ 'target' ], $this -> mOptions[ 'form' ] )
			);

			if ( count( $this -> mWarnings ) > 0 ) {
				$warnings = '';


				foreach ( $this -> mWarnings as $warning ) {
					$warnings .= Xml::tags( 'li', null, $warning );
				}

				$this -> mResult .= wfMsgReplaceArgs(
						$this -> mOptions[ 'warning text' ],
						Xml::tags( 'ul', null, $warnings )
				);
			}
		}
		return $this -> mResult;
	}

	/**
	 * 	Handler for the submit event of a semantic form
	 *
	 * @global <type> $wgOut
	 * @global <type> $wgRequest
	 * @return AjaxResponse
	 */
	function handleSubmit () {
		global $wgOut, $wgRequest;

		// parse options
		$this -> parseDataFromQueryString( $this -> mOptions, $this -> mOptionsString );

		$this -> mOptions[ 'ok text' ] = wfMsg( 'directedit-success' );
		$this -> mOptions[ 'error text' ] = wfMsg( 'directedit-error' );
		$this -> mOptions[ 'warning text' ] = wfMsg( 'directedit-warning' );

		if ( $this -> storeSemanticData( false ) ) {
			$this -> mResult = "SAVED.";
			header( "X-Location: " . $wgOut -> getRedirect() );
			header( "X-Target: " . $this -> mOptions[ 'target' ] );
		}

		return $this -> mResult;
	}

	/**
	 *
	 * @global  $wgOut
	 * @global  $wgRequest
	 * @global <type> $wgUser
	 * @global <type> $wgParser
	 * @return <type>
	 */
	private function storeSemanticData ( $prefillFromExisting = true ) {

		global $wgOut, $wgRequest, $wgUser, $wgParser;

		$oldRequest = $wgRequest;

		// If the wiki is read-only we might as well stop right away
		if ( wfReadOnly ( ) ) {
			$this -> error( array( 'directedit-readonly', wfReadOnlyReason() ) );
			return false;
		}

		// If we have no target article and no form we might as well stop right away
		if ( !array_key_exists( 'target', $this -> mOptions )
			&& !array_key_exists( 'form', $this -> mOptions ) ) {
			$this -> error( 'directedit-notargetspecified' );
			return false;
		}

		// check if form was specified
		if ( !array_key_exists( 'form', $this -> mOptions ) ) {

			// no form specified, find one
			// get title object and id for requested target article
			$title = Title::newFromText( $this -> mOptions[ 'target' ] );
			$form_names = SFFormLinker::getDefaultFormsForPage( $title );

			// if no form can be found, return
			if ( count( $form_names ) == 0 ) {
				$this -> error( 'directedit-noformfound' );
				return false;
			}

			// if more than one form found, issue a warning and continue
			if ( count( $form_names ) > 1 ) {
				$this -> warning( 'directedit-toomanyformsfound' );
			}

			// use the first found form
			$this -> mOptions[ 'form' ] = $form_names[ 0 ];
		}

		// we only care for the form's body
		$wgOut -> setArticleBodyOnly( true );

		$formedit = new SFFormEdit();
		$data = array( );

		////////////////////////////////////////////////////////////////////////
		// First get the Semantic Form and extract its data (if requested)
		// and modify or set as specified in the options string

		if ( $prefillFromExisting ) {

			$wgRequest = new FauxRequest( $this -> mOptions, true );

			// get the Semantic Form
			if ( array_key_exists( 'target', $this -> mOptions ) ) {
				$formedit -> execute( $this -> mOptions[ 'form' ] . '/' . $this -> mOptions[ 'target' ] );
			} else {
				$formedit -> execute( $this -> mOptions[ 'form' ] );
			}

			// extract its data
			$form = $this -> parseDataFromHTMLFrag( $data, trim( $wgOut -> getHTML() ), 'sfForm' );

			if ( !$form ) {
				// something went wrong
				$this -> error( array(
					'directedit-nosemanticform',
					$this -> mOptions[ 'target' ],
					$this -> mOptions[ 'form' ]
					) );
				return false;
			}
		} else {
			$this -> addToArray( $data, "wpSave", "Save" );
		}
		// and modify as specified
		$data = $this ->  array_merge_recursive_distinct($data, $this -> mOptions );

		////////////////////////////////////////////////////////////////////////
		// Second get the MW form, extract its data and GET parameters

		$wgOut -> clearHTML();
		$wgRequest = new FauxRequest( $data, true );

		// get the MW form
		if ( array_key_exists( 'target', $this -> mOptions ) ) {
			$formedit -> execute( $this -> mOptions[ 'form' ] . '/' . $this -> mOptions[ 'target' ] );
		} else {
			$formedit -> execute( $this -> mOptions[ 'form' ] );
		}

		// extract its data
		$data = array( );
		$form = $this -> parseDataFromHTMLFrag( $data, $wgOut -> getHTML(), 'editform' );

		if ( !$form ) {
			// something went wrong
			$this -> error( array(
				'directedit-noeditform',
				$this -> mOptions[ 'target' ],
				$this -> mOptions[ 'form' ]
				) );
			return false;
		}

		// and extract its GET parameters
		$query = explode( '?', $form -> getAttribute( "action" ) );
		if ( count( $query ) > 1 ) {
			$this -> parseDataFromQueryString( $data, $query[ 1 ] );
		}

		// get title object and id for requested target article
		$title = Title::newFromText( $data[ 'title' ] );
		$id = $title -> getArticleId();

		$this -> mOptions[ 'target' ] = $data[ 'title' ];

		// is the user allowed to edit this article?
		$permErrors = $title -> getUserPermissionsErrors( 'edit', $wgUser );

		// And if the article does not yet exist, is he additionally allowed to
		// create it?
		if ( !$title -> exists() ) {
			$permErrors = array_merge( $permErrors,
					wfArrayDiff2( $title -> getUserPermissionsErrors( 'create', $wgUser ), $permErrors ) );
		}

		// if the user lacks permissions, return
		if ( $permErrors ) {
			// Should the message(s) from permError be sent?
			$this -> error( 'directedit-noeditpermission' );
			return false;
		}

		// find existing article if it exists, else create new one
		$article = ( $id != 0 ) ? Article::newFromId( $id ) : new Article( $title );


		////////////////////////////////////////////////////////////////////////
		// Third store article

		$wgOut -> clearHTML();
		$wgRequest = new FauxRequest( $data, true );

		$editor = new EditPage( $article );
		$editor -> importFormData( $wgRequest );

		$resultDetails = false;

		$error = $this -> processEditErrors(
				$editor -> internalAttemptSave( $resultDetails )  // store the article
		);

		if ( $error ) {
			$this -> error( $error );
			return false;
		}

		$wgRequest = $oldRequest;

		return true;
	}

	private function parseDataFromHTMLFrag ( &$data, $html, $formID ) {

		global $wgOut;

		$doc = new DOMDocument();
		@$doc -> loadHTML(
				'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/></head><body>'
				. $html
				. '</body></html>'
		);

		$form = $doc -> getElementById( $formID );

		if ( !$form ) {
			return null;
		}

		// Process input tags
		$inputs = $form -> getElementsByTagName( 'input' );

		for ( $i = 0; $i < $inputs -> length; $i++ ) {

			$input = $inputs -> item( $i );
			$type = $input -> getAttribute( 'type' );
			$name = trim( $input -> getAttribute( 'name' ) );

			if ( !$name )
				continue;

			if ( $type == '' )
				$type = 'text';

			switch ( $type ) {
				case 'checkbox':
				case 'radio':
					if ( $input -> getAttribute( 'checked' ) )
						$this -> addToArray( $data, $name, $input -> getAttribute( 'value' ) );
					break;

				//case 'button':
				case 'hidden':
				case 'image':
				case 'password':
				//case 'reset':
				//case 'submit':
				case 'text':
					$this -> addToArray( $data, $name, $input -> getAttribute( 'value' ) );
					break;

				case 'submit':
					if ( $name == "wpSave" )
						$this -> addToArray( $data, $name, $input -> getAttribute( 'value' ) );
			}
		}

		// Process select tags
		$selects = $form -> getElementsByTagName( 'select' );

		for ( $i = 0; $i < $selects -> length; $i++ ) {

			$select = $selects -> item( $i );
			$name = trim ( $select -> getAttribute( 'name' ) );

			if ( !$name )
				continue;

			$values = array( );
			$options = $select -> getElementsByTagName( 'option' );

			if ( count( $options ) && (!$select -> hasAttribute( "multiple" ) || $options -> item( 0 ) -> hasAttribute( 'selected' ) ) ) {
				$this -> addToArray( $data, $name, $options -> item( 0 ) -> getAttribute( 'value' ) );
			}

			for ( $o = 1; $o < $options -> length; $o++ ) {
				if ( $options -> item( $o ) -> hasAttribute( 'selected' ) )
					$this -> addToArray( $data, $name, $options -> item( $o ) -> getAttribute( 'value' ) );
			}
		}

		// Process textarea tags
		$textareas = $form -> getElementsByTagName( 'textarea' );

		for ( $i = 0; $i < $textareas -> length; $i++ ) {

			$textarea = $textareas -> item( $i );
			$name = trim ( $textarea -> getAttribute( 'name' ) );

			if ( !$name )
				continue;

			$this -> addToArray( $data, $name, $textarea -> textContent );
		}

		return $form;
	}

	/**
	 * Parses data from a query string into the $data array
	 *
	 * @global $wgParser
	 * @global $wgOut
	 * @param Array $data
	 * @param String $queryString
	 * @param Boolean $expand  If this is set to true, field values will get urldecoded and expanded
	 *  This allows to slip parser functions by the MW parser on page creation
	 *  (by urlencoding them) and to pass them to directedit to expand them now.
	 *  Expanding parser functions on page creation already might lead to cache
	 *  issues, e.g. for the {{#time:}} parser function
	 * @return <type> 
	 */
	private function parseDataFromQueryString ( &$data, $queryString, $expand = false ) {

		global $wgParser, $wgOut;

		$params = explode( '&', $queryString );

		foreach ( $params as $i => $param ) {
			$elements = explode( '=', $param, 2 );

			$key = trim( urldecode( $elements[ 0 ] ) );
			$value = count( $elements ) > 1 ? urldecode( $elements[ 1 ] ) : null;

			if ( $key == "query string" ) {
				$this -> parseDataFromQueryString( $data, $value, $expand );
			} elseif ( $expand )  {
				$this -> addToArray( $data, $key, $wgParser -> recursiveTagParse ( $value ) );
			} else  {
				$this -> addToArray( $data, $key, $value );
			}
		}

		return $data;
	}

	// This function recursively inserts the value into a tree.
	// $array is root
	// $key identifies path to position in tree.
	// Format: 1stLevelName[2ndLevel][3rdLevel][...], i.e. normal array notation
	// $value: the value to insert
	private function addToArray ( &$array, $key, $value ) {

		global $wgOut, $wgParser;

		$matches = array( );

		if ( preg_match( '/^([^\[\]]*)\[([^\[\]]*)\](.*)/', $key, $matches ) ) {

			$key = str_replace( ' ', '_', $matches[ 1 ] );
			$key = str_replace( '.', '_', $key );

			if ( !array_key_exists( $key, $array ) )
				$array[ $key ] = array( );

			$this -> addToArray( $array[ $key ], $matches[ 2 ] . $matches[ 3 ], $value );
		} else {

			if ( $key ) {
//				$key = str_replace( ' ', '_', $key );
//				$key = str_replace( '.', '_', $key );
//				var_dump($key);
				$array[ $key ] = $value;
			} else {
				array_push( $array, $value );
			}
		}
	}

	/**
	 * array_merge_recursive merges arrays, but it converts values with duplicate
	 * keys to arrays rather than overwriting the value in the first array with the duplicate
	 * value in the second array, as array_merge does.
	 * 
	 * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
	 * Matching keys' values in the second array overwrite those in the first array.
	 *
	 * Parameters are passed by reference, though only for performance reasons. They're not
	 * altered by this function.
	 * 
	 * See http://www.php.net/manual/en/function.array-merge-recursive.php#92195
	 *
	 * @param array $array1
	 * @param array $array2
	 * @return array
	 * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
	 * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
	 */
	private function array_merge_recursive_distinct ( array &$array1, array &$array2 ) {

	  $merged = $array1;

	  foreach ( $array2 as $key => &$value )
	  {
		if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) )
		{
		  $merged [$key] = $this -> array_merge_recursive_distinct ( $merged [$key], $value );
		}
		else
		{
		  $merged [$key] = $value;
		}
	  }

	  return $merged;
	}

	private function processEditErrors ( $error ) {

		return null;

		switch ( $error ) {
			case EditPage::AS_HOOK_ERROR_EXPECTED:
			case EditPage::AS_CONTENT_TOO_BIG:
			case EditPage::AS_ARTICLE_WAS_DELETED:
			case EditPage::AS_CONFLICT_DETECTED:
			case EditPage::AS_SUMMARY_NEEDED:
			case EditPage::AS_TEXTBOX_EMPTY:
			case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
			case EditPage::AS_END:
			case EditPage::AS_HOOK_ERROR:
			case EditPage::AS_FILTERING:
			case EditPage::AS_SUCCESS_NEW_ARTICLE:
			case EditPage::AS_SUCCESS_UPDATE:
				return null;

			case EditPage::AS_SPAM_ERROR:
				return 'spamprotectiontext';

			case EditPage::AS_BLOCKED_PAGE_FOR_USER:
				return 'blockedtitle';

			case EditPage::AS_IMAGE_REDIRECT_ANON:
				return 'uploadnologin';

			case EditPage::AS_READ_ONLY_PAGE_ANON:
				return 'whitelistedittitle';

			case EditPage::AS_READ_ONLY_PAGE_LOGGED:
			case EditPage::AS_READ_ONLY_PAGE:
				return array( 'directedit-readonly', wfReadOnlyReason() );

			case EditPage::AS_RATE_LIMITED:
				return 'actionthrottledtext';

			case EditPage::AS_NO_CREATE_PERMISSION:
				return 'nocreatetext';

			case EditPage::AS_BLANK_ARTICLE:
				return 'directedit-blankpage';

			case EditPage::AS_IMAGE_REDIRECT_LOGGED:
				return 'badaccess';
		}
	}

	private function error ( $error ) {

		if ( is_array( $error ) ) {
			$text = wfMsg( array_shift( $error ) );
			$args = $error;
		} else {
			$text = wfMsg( $error );
			$args = null;
		}

		$this -> mResult = new AjaxResponse( wfMsgReplaceArgs( $this -> mOptions[ 'error text' ], array( wfMsgReplaceArgs( $text, $args ) ) ) );
		$this -> mResult -> setResponseCode( '400 Bad Request' );
		return $this -> mResult;
	}

	private function warning ( $error ) {

		if ( is_array( $error ) ) {
			$text = wfMsg( array_shift( $error ) );
			$args = $error;
		} else {
			$text = wfMsg( $error );
			$args = null;
		}

		$this -> mWarnings[ ] = wfMsgReplaceArgs( $text, $args );
	}

}

